////////////////////////////////////////////////////////////////////////////////
//
//  MICROSOFT CORPORATION
//  Copyright (c) Microsoft Corporation.
//  All Rights Reserved.
//
//  NOTICE: Microsoft Confidential. Intended for Internal Use Only.
//
////////////////////////////////////////////////////////////////////////////////

package com.msn.beet.core
{

import com.msn.beet.errors.ApplicationError;

import flash.display.DisplayObject;
import flash.display.MovieClip;
import flash.errors.IllegalOperationError;
import flash.events.Event;
import flash.events.IOErrorEvent;
import flash.events.ProgressEvent;
import flash.events.SecurityErrorEvent;
import flash.net.URLLoader;
import flash.net.URLRequest;
import flash.utils.Dictionary;
import flash.utils.getDefinitionByName;
import flash.utils.getQualifiedClassName;

/**
 * The ApplicationRuntime acts as the application host that launches the
 * main Application at run time. This includes pre-loading the main
 * Application as well as initializing it.
 * <p>Extend this class in your project and override applicationLoadStart(),
 * applicationLoadProgress(), and applicationLoadComplete() for a custom
 * preloader sequence. These function are invoked automatically by the
 * ApplicationRuntime base class.</p>
 */
public class ApplicationRuntime extends MovieClip
{
    include "../core/Version.as";

    //-------------------------------------------------------------------------
    //
    //  Class Variables
    //
    //-------------------------------------------------------------------------

    /**
    * @private
    * Prevent multiple instantiations
    */
    private static var initialized:Boolean = false;

    //-------------------------------------------------------------------------
    //
    //  Constructor
    //
    //-------------------------------------------------------------------------

    /**
     * Base constructor
     */
    public function ApplicationRuntime(configuration:XML)
    {
        super.stop();
        preventMultipleInstantiations();
        registerLoaderInfoEventHandlersIfSWFNotLoaded();
        doApplicationSetup(configuration);
        loadExternalConfigFileIfRequired();
        doImmediateInitializationIfRequired();
    }

    //-------------------------------------------------------------------------
    //
    //  Variables
    //
    //-------------------------------------------------------------------------

    /**
     * Name of the document class. This can be overridden in the inherited
     * class constructor
     */
    protected var documentClassName:String = "Main";

    /**
     * Reference of the document object. This can be accessed in the inherited
     * class
     */
    protected var documentObject:DisplayObject = null;

    private var externalConfigItem:URLLoader = null;
    private var externalConfigLoadFinished:Boolean = true;

    //-------------------------------------------------------------------------
    //
    //  Properties
    //
    //-------------------------------------------------------------------------


    //----------------------------------
    //
    //  bytesLoaded
    //
    //----------------------------------

    /**
     * The amount of data that has been loaded
     * @return uint
     */
    protected final function get bytesLoaded():uint
    {
        return loaderInfo.bytesLoaded;
    }

    //----------------------------------
    //
    //  bytesTotal
    //
    //----------------------------------

    /**
     * The total amount of data that is being preloaded
     * @return uint
     */
    protected final function get bytesTotal():uint
    {
        return loaderInfo.bytesTotal;
    }

    //-------------------------------------------------------------------------
    //
    //  Template Methods
    //
    //-------------------------------------------------------------------------

    /**
     * Invoked when the preloading begins.
     * Override this method in your project
     */
    protected function applicationLoadStart():void
    {
        // override this
    }

    /**
     * Invoked every time data is downloaded
     * Override this method in your project
     */
    protected function applicationLoadProgress():void
    {
        // override this
    }

    /**
     * Invoked when the preloading is complete
     * Override this method in your project
     */
    protected function applicationLoadComplete():void
    {
        // override this
    }

    /**
     * Invoked when an error occurs during preloading
     * Override this method in your project
     */
    protected function applicationLoadError(e:ApplicationError):void
    {
        // override this
    }

    //-------------------------------------------------------------------------
    //
    //  Methods
    //
    //-------------------------------------------------------------------------

    private function buildDictionary(key:String, value:String,
                                     list:XMLList):Dictionary
    {
        var dict:Dictionary = new Dictionary();
        for (var i:int = 0, n:uint = list.length(); i < n; i++)
        {
            dict[String(list[i].@[key])] = String(list[i].@[value]);
        }
        return dict;
    }

    private function destroyLoader():void
    {
        loaderInfo.removeEventListener(ProgressEvent.PROGRESS,
                                       loaderInfo_progressHandler);
        loaderInfo.removeEventListener(Event.INIT, loaderInfo_initHandler);
        loaderInfo.removeEventListener(IOErrorEvent.IO_ERROR,
                                       loaderInfo_ioErrorHandler);
        loaderInfo.removeEventListener(Event.COMPLETE,
                                       loaderInfo_completeHandler);
    }

    private function destroyURLLoader():void
    {
        this.externalConfigItem.removeEventListener(
            Event.COMPLETE, externalConfigItem_completeHandler);
        this.externalConfigItem.removeEventListener(
            IOErrorEvent.IO_ERROR, externalConfigItem_ioErrorEventHandler);
        this.externalConfigItem.removeEventListener(
            IOErrorEvent.NETWORK_ERROR, externalConfigItem_ioErrorEventHandler);
        this.externalConfigItem.removeEventListener(
            SecurityErrorEvent.SECURITY_ERROR,
            externalConfigItem_ioErrorEventHandler);
        this.externalConfigItem = null;
    }

    private function doApplicationSetup(configuration:XML):void
    {
        Application.initialize(this);

        try
        {
            Application.config(configuration);
        }
        catch(e:ApplicationError)
        {
            applicationLoadError(e);
        }
    }

    private function doImmediateInitializationIfRequired():void
    {
        if (this.externalConfigItem || !isApplicationSWFLoaded())
        {
            return;
        }

        // Assuming the swf is fully loaded and there is no external file,
        // invoke the application initialization after a one frame beat.
        var enterFrameHandler:Function;
        enterFrameHandler = function(e:Event):void
        {
            removeEventListener(Event.ENTER_FRAME, enterFrameHandler);
            init();
        };
        addEventListener(Event.ENTER_FRAME, enterFrameHandler);
    }

    private function init():void
    {
        initDictionaries();
        nextFrame();
        startMainApplication();
        applicationLoadComplete();
    }

    private function initDictionaries():void
    {
        // build dictionaries
        var feeds:Dictionary = buildDictionary(
            "name", "url", Application.externalSettings.feeds.feed);
        var assets:Dictionary = buildDictionary(
            "name", "url", Application.externalSettings.assets.asset);
        var projectSettings:Dictionary = buildDictionary(
            "key", "value",
            Application.externalSettings.projectSettings.item);
        Application.setDictionaries(feeds, assets, projectSettings);
    }

    private function isApplicationSWFLoaded():Boolean
    {
        return this.loaderInfo.bytesLoaded == this.loaderInfo.bytesTotal;
    }

    private function loadExternalConfigFileIfRequired():void
    {
        if (Application.externalConfigPath == "")
        {
            return;
        }

        this.externalConfigLoadFinished = false;
        this.externalConfigItem = new URLLoader();
        this.externalConfigItem.addEventListener(
            Event.COMPLETE,
            externalConfigItem_completeHandler);
        this.externalConfigItem.addEventListener(
            IOErrorEvent.IO_ERROR,
            externalConfigItem_ioErrorEventHandler);
        this.externalConfigItem.addEventListener(
            IOErrorEvent.NETWORK_ERROR,
            externalConfigItem_ioErrorEventHandler);
        this.externalConfigItem.addEventListener(
            SecurityErrorEvent.SECURITY_ERROR,
            externalConfigItem_ioErrorEventHandler);
        this.externalConfigItem.load(new URLRequest(
            Application.externalConfigPath))
    }

    private function mergeXML(newXML:XML):void
    {
        // update feeds
        Application.externalSettings.feeds.feed = updateCollection(
            "name", "url", Application.externalSettings.feeds.feed,
            newXML.feeds.feed);

        // update assets
        Application.externalSettings.assets.asset = updateCollection(
            "name", "url", Application.externalSettings.assets.asset,
            newXML.assets.asset);

        // update projectSettings
        Application.externalSettings.projectSettings.item =
            updateCollection(
                "key", "value",
                Application.externalSettings.projectSettings.item,
                newXML.projectSettings.item);
    }

    private function preventMultipleInstantiations():void
    {
        if (ApplicationRuntime.initialized)
        {
            throw new IllegalOperationError(getQualifiedClassName(this) +
                                            " can only have one instance.");
        }

        ApplicationRuntime.initialized = true;
    }

    private function registerLoaderInfoEventHandlersIfSWFNotLoaded():void
    {
        // HACK: It appears that if the swf is already cached, the loaderInfo
        // event handlers may not fire in certain browsers (like FF3). This
        // issue requires further analysis to confirm. --ryanwill
        if (isApplicationSWFLoaded())
        {
            return;
        }

        this.loaderInfo.addEventListener(ProgressEvent.PROGRESS,
                                         loaderInfo_progressHandler);
        this.loaderInfo.addEventListener(Event.INIT, loaderInfo_initHandler);
        this.loaderInfo.addEventListener(IOErrorEvent.IO_ERROR,
                                         loaderInfo_ioErrorHandler);
        this.loaderInfo.addEventListener(Event.COMPLETE,
                                         loaderInfo_completeHandler);
    }

    private function startMainApplication():void
    {
        // set start time
        Application.setStartTime(new Date())

        // run the main application
        try
        {
            var mainClass:Class =
                Class(getDefinitionByName(this.documentClassName));
            documentObject = new mainClass() as DisplayObject;
            addChild(documentObject);
        }
        catch (e:TypeError)
        {
            throw new ApplicationError(
                "Entry point class was null pointer or not a DisplayObject. " +
                "Unable to start application ",
                ApplicationError.FAILED_TO_LOAD_MAIN_APP);
        }
        catch (e:ReferenceError)
        {
            throw new ApplicationError(
                "Missing entry point class '" + this.documentClassName
                + "'. Unable to start application ",
                ApplicationError.MISSING_ENTRY_POINT);
        }
    }

    private function updateCollection(key:String, value:String,
                                      xmlList1:XMLList,
                                      xmlList2:XMLList):XMLList
    {
        var n:uint = xmlList2.length();
        for (var i:uint = 0; i < n; i++)
        {
            var target:String = xmlList2[i].@[key];
            xmlList1.(@[key] == target).@[value] =
                xmlList2.(@[key] == target).@[value][0];
        }

        return xmlList1;
    }

    //-------------------------------------------------------------------------
    //
    //  Event Handlers
    //
    //-------------------------------------------------------------------------

    private function externalConfigItem_completeHandler(e:Event):void
    {
        try
        {
            var externalConfigXML:XML = XML(this.externalConfigItem.data);
            //apply SecuritySettings again which is defined in externalConfigXML
            var securitySettings:XMLList =
                externalConfigXML.appSettings.security;
            Application.applySecuritySettings(securitySettings);

            var externalSettings:XML =
                XML(externalConfigXML.externalSettings);
            mergeXML(externalSettings);
        }
        catch(e:TypeError)
        {
            applicationLoadError(
                new ApplicationError(
                    "Malformed external config. " + e.message,
                    ApplicationError.EXTERNAL_CONFIG_MALFORMED));
        }

        this.externalConfigLoadFinished = true;
        destroyURLLoader();

        if (isApplicationSWFLoaded())
        {
            init();
        }
    }

    private function externalConfigItem_ioErrorEventHandler(e:Event):void
    {
        destroyURLLoader();
        applicationLoadError(
            new ApplicationError(
                "Failed to load XML from: "
                + Application.externalConfigPath,
                ApplicationError.FAILED_TO_LOAD_EXTERNAL_CONFIG));
    }

    private function loaderInfo_completeHandler(e:Event):void
    {
        destroyLoader();

        if (this.externalConfigLoadFinished)
        {
            init();
        }
    }

    private function loaderInfo_initHandler(e:Event):void
    {
        applicationLoadStart();
    }

    private function loaderInfo_ioErrorHandler(e:Event):void
    {
        destroyLoader();
        applicationLoadError(new ApplicationError(
                "Failed to load main application",
                ApplicationError.FAILED_TO_LOAD_MAIN_APP));
    }

    private function loaderInfo_progressHandler(e:ProgressEvent):void
    {
        applicationLoadProgress();
    }

}

}
